package mspbots.smileback.client.oauth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.smileback.client.annotation.ApiException;
import mspbots.smileback.client.core.client.ClientUtils;
import mspbots.smileback.common.RedisKeys;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * com.mspbots.doctor.core.AuthService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/3/19
 */
@Log4j2
@Service
public class AuthService {

    private final AuthProperties authProperties;
    private final WebClient webClient;
    private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
    private final ObjectMapper objectMapper;

    public AuthService(AuthProperties authProperties, WebClient.Builder builder,
                       ReactiveRedisTemplate<String, Object> reactiveRedisTemplate, ObjectMapper objectMapper) {
        this.authProperties = authProperties;
        this.webClient = builder.baseUrl(authProperties.getEndpoints()).build();
        this.reactiveRedisTemplate = reactiveRedisTemplate;
        this.objectMapper = objectMapper;
    }

    public Mono<String> authToken(Long tenantId, String code) {
        Map<String, Object> params = new HashMap<>(10);
        params.put("grant_type", "authorization_code");
        params.put("redirect_uri", this.authProperties.getRedirectUri());
        params.put("code", code);
        return this.tokenClientGet(tenantId, params)
                .map(AuthorizationCode::getAccessToken);
    }

    public Mono<String> getToken(Long tenantId) {

        Mono<String> accessTokenMono = this.reactiveRedisTemplate.opsForValue()
                .get(RedisKeys.ACCESS_TOKEN_TENANT_REDIS_KEY_PREFIX + tenantId)
                .map(result -> this.objectMapper.convertValue(result, String.class));

        Mono<AuthorizationCode> authorizationCodeMono = this.reactiveRedisTemplate.opsForValue()
                .get(RedisKeys.OAUTH_TENANT_REDIS_KEY_PREFIX + tenantId)
                .map(result -> this.objectMapper.convertValue(result, AuthorizationCode.class));

        return accessTokenMono.switchIfEmpty(authorizationCodeMono
                .flatMap(authorizationCode -> this.refreshToken(tenantId, authorizationCode.getRefreshToken()))
                .map(AuthorizationCode::getAccessToken))
                .switchIfEmpty(Mono.error(new NoAuthorizationException(
                            this.objectMapper.createObjectNode().put("error", "This tenant [" + tenantId + "] " +
                                    "access token is empty, Allow access your account."))));

    }

    private Mono<AuthorizationCode> refreshToken(Long tenantId, String refreshToken) {
        Map<String, Object> params = new HashMap<>(10);
        params.put("grant_type", "refresh_token");
        params.put("refresh_token", refreshToken);
        return this.tokenClientGet(tenantId, params);
    }


    private Mono<AuthorizationCode> tokenClientGet(Long tenantId, Map<String, Object> params) {
        log.debug("Post token params:{}", params);
        return this.webClient.post().uri(uriBuilder -> uriBuilder.path("/api/token/").build())
                .headers(httpHeaders -> httpHeaders.setBasicAuth(this.authProperties.getClientId(),
                        this.authProperties.getClientSecret()))
                .accept(MediaType.APPLICATION_FORM_URLENCODED)
                .bodyValue(ClientUtils.form(params))
                .retrieve()
                .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                        .map(errMsg -> new ApiException(errMsg, clientResponse.statusCode())))
                .bodyToMono(JsonNode.class)
                .map(jsonNode -> AuthorizationCode.builder().accessToken(jsonNode.get("access_token").asText())
                        .scope(jsonNode.get("scope").asText())
                        .tenantId(tenantId)
                        .refreshToken(jsonNode.get("refresh_token").asText())
                        .expiresIn(jsonNode.get("expires_in").asLong())
                        .tokenType(jsonNode.get("token_type").asText())
                        .build())
                .doOnSuccess(authCode -> this.reactiveRedisTemplate.opsForValue()
                        .set(RedisKeys.OAUTH_TENANT_REDIS_KEY_PREFIX + tenantId,
                                authCode, Duration.ofDays(36000))
                        .mergeWith(this.reactiveRedisTemplate.opsForValue()
                                .set(RedisKeys.ACCESS_TOKEN_TENANT_REDIS_KEY_PREFIX + tenantId,
                                        authCode.getAccessToken(), Duration.ofSeconds(authCode.getExpiresIn())))
                        .subscribe());
    }

    private static class NoAuthorizationException extends ApiException {

        public NoAuthorizationException(JsonNode message) {
            super(message, HttpStatus.NOT_FOUND);
        }
    }

}
